package com.gitee.chessboard.scan;

import com.gitee.chessboard.scan.model.JarFileModel;
import com.gitee.chessboard.utils.PathUtils;
import com.gitee.chessboard.utils.StringUtils;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.jar.JarEntry;

/**
 * 扫描处理器
 *
 * @author 敲代码的旺财
 * @date 2021/12/7 10:17
 */
public class ScanHandler {

    /**
     * 扫描到的资源路径
     */
    private URL[] urls;

    /**
     * 需要扫描的路径
     */
    private String basePackage;

    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

    String DEFAULT_RESOURCE_PATTERN = "**/*.class";

    String LOCAL_RESOURCE_PATTERN = "*.class";

    public ScanHandler() {
    }

    public ScanHandler(String basePackage) {
        this.basePackage = basePackage;
    }

    public URL[] scan() {
        if (this.urls == null) {
            this.urls = findSpecifyPath();
        }
        return this.urls;
    }

    public URL[] findSpecifyPath() {
        //将包路径，转成文件路径
        //子路径下的文件
        String packageSearchPath = CLASSPATH_ALL_URL_PREFIX +
                basePackage.replace(".", "/") + '/' + DEFAULT_RESOURCE_PATTERN;
        //扫描当前路径下的文件
        String localPackageSearchPath = CLASSPATH_ALL_URL_PREFIX +
                basePackage.replace(".", "/") + '/' + LOCAL_RESOURCE_PATTERN;
        URL[] resourcesInPath = getResourcesInPath(packageSearchPath);
        URL[] localResourcesInPath =getResourcesInPath(localPackageSearchPath);
        //合并两个结果
        URL[] result = Arrays.copyOf(resourcesInPath, resourcesInPath.length + localResourcesInPath.length);
        System.arraycopy(localResourcesInPath, 0, result, resourcesInPath.length, localResourcesInPath.length);
        return result;
    }

    public URL[] getResourcesInPath(String packageSearchPath) {
        //判断是否是package路径
        if (packageSearchPath.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
            //以classpath*:说明是个package路径
            String determineSearchPath = packageSearchPath.substring(CLASSPATH_ALL_URL_PREFIX.length());
            //判断是否是个通配符的路径
            if (PathUtils.isPatternPath(determineSearchPath)) {
                //是一个通配符路径，这种路径需要解析指定范围的所有路径
                return findPathMatchingResources(packageSearchPath);
            } else {
                //不是通配符路径，这是一个明确的路径，需要解析路径下的所有class
                return findAllClassPathResources(determineSearchPath);
            }
        } else {
            //不是一个package路径，是一个jar包路径，或者是一个war包路径，亦或者是一个具体的文件
            int prefixEnd = (packageSearchPath.startsWith("war:") ? packageSearchPath.indexOf("*/") + 1 :
                    packageSearchPath.indexOf(':') + 1);
            if (PathUtils.isPatternPath(packageSearchPath.substring(prefixEnd))) {
                //通配符的路径
                return findPathMatchingResources(packageSearchPath);
            } else {
                //不是通配符的路径
                return findAllClassPathResources(packageSearchPath);
            }
        }
    }

    /**
     * 查询所有的通配符路径下的资源
     *
     * @return 资源路径
     */
    protected URL[] findPathMatchingResources(String packageSearchPath) {
        //获取前缀路径，也就是通配符之前的明确的那一部分的路径，/WEB-INF/*.xml为例，/WEB-INF/就是前缀部分
        String rootDirPath = PathUtils.getDeterminePrefixPath(packageSearchPath);
        //获取路径通配符部分
        String subPattern = packageSearchPath.substring(rootDirPath.length());
        Set<URL> result = new LinkedHashSet<>(16);
        //获取前缀部分的下所有的package路径
        URL[] rootDirPathUrls = getResourcesInPath(rootDirPath);
        for (URL rootDirPathUrl : rootDirPathUrls) {
            String protocol = rootDirPathUrl.getProtocol();
            //根据不同的资源类型进行扫描
            if ("jar".equals(protocol) || "war".equals(protocol)) {
                //程序包  jar或者war类型的包
                result.addAll(findPathMatchingJarResources(rootDirPathUrl, subPattern));
            } else {
                //直接的文件类型
                result.addAll(findPathMatchingFileResources(rootDirPathUrl, subPattern));
            }
        }
        return result.toArray(new URL[0]);
    }

    /**
     * 查询明确路径下的所有资源
     *
     * @return 资源路径
     */
    protected URL[] findAllClassPathResources(String determineSearchPath) {
        try {
            //这里必须去掉前面的/
            if (determineSearchPath.startsWith("/")) {
                determineSearchPath = determineSearchPath.substring(1);
            }
            Set<URL> result = new LinkedHashSet<>(16);
            //直接通过类加载器读取 jar、war中的资源
            Enumeration<URL> resourceUrls = ClassLoader.getSystemResources(determineSearchPath);

            while (resourceUrls.hasMoreElements()) {
                //重新封装资源
                URL url = resourceUrls.nextElement();
                result.add(url);
            }
            return result.toArray(new URL[0]);
        } catch (IOException e) {
            throw new RuntimeException("扫描资源失败", e);
        }
    }

    /**
     * 搜索数据包中的资源
     *
     * @param rootDirURL 数据包的路径
     * @param subPattern 匹配路径
     * @return 数据包中的指定资源
     */
    protected Set<URL> findPathMatchingJarResources(URL rootDirURL, String subPattern) {
        try (JarFileModel jarFileModel = new JarFileModel.Builder().setUrl(rootDirURL).build()) {
            if (!"".equals(jarFileModel.getRootEntryPath()) && !jarFileModel.getRootEntryPath().endsWith("/")) {
                // 这里的路径必须是以斜杠为结尾
                jarFileModel.setRootEntryPath(jarFileModel.getRootEntryPath() + "/");
            }
            Set<URL> result = new LinkedHashSet<>(8);
            for (Enumeration<JarEntry> entries = jarFileModel.getJarFile().entries(); entries.hasMoreElements(); ) {
                //获取jar包中的资源实例
                JarEntry entry = entries.nextElement();
                String entryPath = entry.getName();
                if (entryPath.startsWith(jarFileModel.getRootEntryPath())) {
                    String relativePath = entryPath.substring(jarFileModel.getRootEntryPath().length());
                    //TODO match可能会有问题
                    if (PathUtils.match(subPattern, relativePath)) {
                        if (relativePath.startsWith("/")) {
                            relativePath = relativePath.substring(1);
                        }
                        relativePath = StringUtils.replace(relativePath, "#", "%23");
                        result.add(new URL(rootDirURL, relativePath));
                    }
                }
            }
            return result;
        } catch (IOException e) {
            throw new RuntimeException("扫描资源失败", e);
        }
    }

    /**
     * 搜索指定路径下所得符合匹配要求的文件
     *
     * @param rootDirURL 指定的路径
     * @param subPattern 匹配要求
     * @return 资源路径
     */
    protected Set<URL> findPathMatchingFileResources(URL rootDirURL, String subPattern) {
        //这里需要重新转码否则  url中的中文就是url编码
        String path = URLDecoder.decode(rootDirURL.getFile(), StandardCharsets.UTF_8);
        //获取文件地址
        File rootDir = new File(path).getAbsoluteFile();
        Set<File> matchingFiles = new HashSet<>();

        if (rootDir.exists() && rootDir.isDirectory() && rootDir.canRead()) {
            //如果是一个文件夹，就需要继续解析里面的内容
            String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");
            if (!subPattern.startsWith("/")) {
                fullPattern += "/";
            }
            fullPattern = fullPattern + StringUtils.replace(subPattern, File.separator, "/");
            matchingFiles(fullPattern, rootDir, matchingFiles);
        }
        Set<URL> result = new LinkedHashSet<>(matchingFiles.size());
        try {
            for (File file : matchingFiles) {
                result.add(file.toURI().toURL());
            }
        } catch (MalformedURLException e) {
            throw new RuntimeException("File转URL失败", e);
        }
        return result;
    }

    /**
     * 在指定的目录下递归查询匹配路径要求的文件
     *
     * @param fullPattern 匹配要求
     * @param dir         需要搜索的文件夹
     * @param result      符合路径要求的文件
     */
    private void matchingFiles(String fullPattern, File dir, Set<File> result) {
        //获取文件夹下的所有文件和文件夹
        File[] files = Optional.ofNullable(dir.listFiles()).orElse(new File[0]);
        //根据名称排序
        Arrays.sort(files, Comparator.comparing(File::getName));
        for (File file : files) {
            //整理文件路径格式
            String currPath = StringUtils.replace(file.getAbsolutePath(), File.separator, "/");
            if (file.isDirectory() && PathUtils.match(fullPattern, currPath + "/")) {
                //如果是文件夹  并且依旧 在匹配路径中，继续深入子文件夹
                if (file.canRead()) {
                    matchingFiles(fullPattern, file, result);
                }
            }
            if (PathUtils.match(fullPattern, currPath)) {
                //只要是符合要求的文件或者文件夹 都放入集合中
                result.add(file);
            }
        }
    }

    public URL[] getUrls() {
        return urls;
    }

    public void setUrls(URL[] urls) {
        this.urls = urls;
    }

    public String getBasePackage() {
        return basePackage;
    }

    public void setBasePackage(String basePackage) {
        this.basePackage = basePackage;
    }
}
